在深度學習應用中,模型的載入速度和推理效率至關重要,尤其是在大規模部署和高併發場景下。直接從雲端儲存(例如 Google Cloud Storage,GCS)載入模型往往會成為效能瓶頸。
本文將探討如何利用 Google Kubernetes Engine (GKE) 上的本地 NVMe SSD 和 HostPath,搭配 RAID 0 配置,構建一個高效能的深度學習推理環境。
我們將逐步講解如何創建一個配備 MIG H100 GPU 的節點池,如何使用本地卷靜態預配工具設定 RAID 並格式化磁碟,以及如何利用 DaemonSet 自動將模型自動從 GCS 同步到節點的本地 NVMe SSD。最後,我們將部署一個大規模的推理任務,並分析這種方法相比直接從 GCS 載入模型的優勢,例如降低延遲、減少網路頻寬消耗、簡化 Pod 設定以及更好的資源利用。
使用 Day3 範例創建的Cluster,再建立一個 MIG H100x8(a3-highgpu-8g
)切分為1g.10gb
的 Node Pool,MIG GPU 可以看 Day25 的介紹,其中將 local_nvme_ssd_count 設定為 16,這將會自動預配本地固態硬盤容量共 16顆各 375GB 的 local ssd 硬碟。
### node-pool-variables.tf
module "gke" {
node_pools = [
var.h100-partition-7.config,
]
node_pools_labels = {
"${var.h100-partition-7.config.name}" = var.h100-partition-7.kubernetes_label
}
node_pools_taints = {
"${var.h100-partition-7.config.name}" = var.h100-partition-7.taints
}
node_pools_resource_labels = {
"${var.h100-partition-7.config.name}" = var.h100-partition-7.node_pools_resource_labels
}
}
### Node pool
variable "h100-partition-7" {
default = {
config = {
name = "h100-partition-7"
machine_type = "a3-highgpu-8g"
accelerator_type = "nvidia-h100-80gb"
accelerator_count = "8"
# 將每張 H100 切分成 7 份
gpu_partition_size = "1g.10gb"
gpu_driver_version = "LATEST"
node_locations = "us-central1-c"
# 每個 Node 可以部署 256 個 Pods
max_pods_per_node = 256
autoscaling = false
node_count = 1
local_ssd_count = 0
disk_size_gb = 2000
local_nvme_ssd_count = 16
spot = true
disk_type = "pd-ssd"
image_type = "COS_CONTAINERD"
enable_gcfs = false
enable_gvnic = false
logging_variant = "DEFAULT"
auto_repair = true
auto_upgrade = true
preemptible = false
}
kubernetes_label = {
role = "h100-mig"
}
taints = [
key = "nvidia/share"
value = "nvidia-mig"
effect = "NO_SCHEDULE"
]
}
}
進入 Node VM 中使用指令 sudo fdisk -l
查看硬碟和分割區,以及它們的大小和類型。可以看到有 16 個 375 GiB 的硬碟 (nvme0~15) 以及 1.94 GiB 的開機硬碟。
$ sudo fdisk -l
### ...以上省略...
Disk /dev/nvme15n1: 375 GiB, 402653184000 bytes, 98304000 sectors
Disk model: nvme_card14
Units: sectors of 1 * 4096 = 4096 bytes
Sector size (logical/physical): 4096 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes
Disk /dev/mapper/vroot: 1.94 GiB, 2087714816 bytes, 509696 sectors
Units: sectors of 1 * 4096 = 4096 bytes
Sector size (logical/physical): 4096 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes
可以使用本地卷靜態預配工具自動為本地 SSD 建立 PersistentVolume。 預配工具是一個 DaemonSet
,用於管理每個節點上的本地固態硬碟磁碟、為它們創建和刪除 PersistentVolume 以及在釋放 PersistentVolume 時清除本地固態硬碟磁碟上的數據。
要執行本地卷靜態預配程式,請執行以下操作:
使用 DaemonSet 設定 RAID 並對磁碟進行格式化:
部署RAID磁碟DaemonSet。 DaemonSet 會在所有本地 SSD 磁碟上設置 RAID 0 陣列,並將設備格式化為 ext4
檔案系統。
kubectl create -f gke-daemonset-raid-disks.yaml
# gke-daemonset-raid-disks.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: gke-raid-disks
namespace: default
labels:
k8s-app: gke-raid-disks
spec:
selector:
matchLabels:
name: gke-raid-disks
template:
metadata:
labels:
name: gke-raid-disks
spec:
nodeSelector:
cloud.google.com/gke-local-nvme-ssd: "true"
hostPID: true
containers:
- name: startup-script
image: gcr.io/google-containers/startup-script:v1
securityContext:
privileged: true
env:
- name: STARTUP_SCRIPT
value: |
set -o errexit
set -o nounset
set -o pipefail
devices=()
for ssd in /dev/disk/by-id/google-local-ssd-block*; do
if [ -e "${ssd}" ]; then
devices+=("${ssd}")
fi
done
if [ "${#devices[@]}" -eq 0 ]; then
echo "No Local NVMe SSD disks found."
exit 0
fi
seen_arrays=(/dev/md/*)
device=${seen_arrays[0]}
echo "Setting RAID array with Local SSDs on device ${device}"
if [ ! -e "$device" ]; then
device="/dev/md/0"
echo "y" | mdadm --create "${device}" --level=0 --force --raid-devices=${#devices[@]} "${devices[@]}"
fi
if ! tune2fs -l "${device}" ; then
echo "Formatting '${device}'"
mkfs.ext4 -F "${device}"
fi
mountpoint=/mnt/disks/raid/0
mkdir -p "${mountpoint}"
echo "Mounting '${device}' at '${mountpoint}'"
mount -o discard,defaults "${device}" "${mountpoint}"
chmod a+w "${mountpoint}"
tolerations:
- operator: "Exists"
effect: NoSchedule
查看 gke-raid-disks Pod Log (kubectl logs $gke-raid-disks-Pod-Name
)
$ kubectl logs $gke-raid-disks-Pod-Name
Setting RAID array with Local SSDs on device /dev/md/*
mdadm: Defaulting to version 1.2 metadata
mdadm: array /dev/md/0 started.
tune2fs: Bad magic number in super-block while trying to open /dev/md/0
tune2fs 1.47.0 (5-Feb-2023)
Formatting '/dev/md/0'
mke2fs 1.47.0 (5-Feb-2023)
Discarding device blocks: done
Creating filesystem with 1572335616 4k blocks and 196542464 inodes
Filesystem UUID: 7797058c-7483-45e6-85c4-c3746cfb23dc
Superblock backups stored on blocks:
32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,
4096000, 7962624, 11239424, 20480000, 23887872, 71663616, 78675968,
102400000, 214990848, 512000000, 550731776, 644972544
Allocating group tables: done
Writing inode tables: done
Creating journal (262144 blocks): done
Writing superblocks and filesystem accounting information: done
Mounting '/dev/md/0' at '/mnt/disks/raid/0'
!!! startup-script succeeded!
確認 Pod 中的 Shell Script 執行完成後,使用 sudo fdisk -l
指令查看磁碟分區,會發現 16 個 375 Gi 的硬碟 Raid0 後總共組成了 5.86 TiB。
$ sudo fdisk -l
### ...以上省略...
Disk /dev/md0: 5.86 TiB, 6440286683136 bytes, 1572335616 sectors
Units: sectors of 1 * 4096 = 4096 bytes
Sector size (logical/physical): 4096 bytes / 4096 bytes
I/O size (minimum/optimal): 524288 bytes / 8388608 bytes
這個 DaemonSet 的目的是將 GCS 存儲桶中的 Model 定期同步到集群中每個 GPU 節點的本地 NVMe SSD RAID 設備(hostPath)的 /mnt/disks/raid/0
目錄中,每當增加新的 GPU 節點時,都會自動部署此 DaemonSet Auto Sync 的 Pod,自動將所有 GPU 節點的 hostPath 都同步 Model 上去,如下圖。以下 Model 及 GCS 使用 Day23 訓練出來的 MNIST Model
sync-model.yaml
有以下幾個重點:
$K8s_Service_Account
變數改成具有存取 Model GCS 權限的 Service Account。$PROJECT_ID
變數改成 Model GCS 所在的專案 ID。$GCS_BUCKET_NAME
變數改成 Model GCS 名稱。SYS_ADMIN
參數之後執行 kubectl apply -f sync-model.yaml
指令
# sync-model.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: model-sync
namespace: ai
spec:
selector:
matchLabels:
app: model-sync
template:
metadata:
labels:
app: model-sync
spec:
# 填入剛剛創建的 K8s SA 具有 GCS 物件擁有者權限
serviceAccountName: "$K8s_Service_Account"
initContainers:
- name: init
image: busybox:1.28
command: ['sh', '-c', 'echo "Suspend for 60 seconds to wait for the RAID device to be ready." && sleep 60']
containers:
- name: model-sync
image: google/cloud-sdk:stable
imagePullPolicy: IfNotPresent
securityContext:
privileged: true
capabilities:
add:
- SYS_ADMIN
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "1"
memory: "2Gi"
env:
- name: PROJECT_ID
value: "$PROJECT_ID"
- name: GCS_BUCKET_NAME
value: "$GCS_BUCKET_NAME"
- name: DESTINATION
value: "/model-sync-volume"
command:
- /bin/bash
- -c
- |
while :
do
if [ ! -d $DESTINATION ]; then
mkdir -p $DESTINATION
fi
echo "Starts download model weights..."
gcloud config set project $PROJECT_ID
mkdir -p $DESTINATION
gcloud storage rsync --recursive gs://$GCS_BUCKET_NAME $DESTINATION
echo "Process finished"
sleep 600
done
volumeMounts:
- name: model-sync-volume
mountPropagation: "Bidirectional"
mountPath: "/model-sync-volume"
volumes:
- name: model-sync-volume
hostPath:
path: /mnt/disks/raid/0
type: DirectoryOrCreate
nodeSelector:
cloud.google.com/gke-local-nvme-ssd: "true"
tolerations:
- operator: "Exists"
effect: NoSchedule
SSH 進入 GPU 的 Node 中查看 /mnt/disks/raid/0
目錄下是否存在同步的 Model
gke-demo-ai-cluster-h100-partition-7 /tmp $ cd /mnt/disks/raid/0
gke-demo-ai-cluster-h100-partition-7 /mnt/disks/raid/0 $ ls -al
total 32
drwxrwxrwx 6 root root 4096 Oct 8 11:47 .
drwxr-xr-x 3 root root 60 Oct 7 11:48 ..
drwx------ 2 root root 16384 Oct 7 11:48 lost+found
drwxr-xr-x 2 root root 4096 Oct 8 11:47 mnist_predict
drwxr-xr-x 2 root root 4096 Oct 8 11:47 mnist_saved_model
drwxr-xr-x 4 root root 4096 Oct 8 11:47 tensorflow-mnist-example
使用 Day23 的 MNIST 數據部署推理 Job,將以下值改寫:
spec.parallelism
改成 60,最多可以同時執行 60 個 Podcloud.google.com/gke-accelerator: nvidia-h100-80gb
使用我們創建的 MIG H100 Nodevolumes
將剛剛 Model Sync 的 hostPath 掛載進來volumeMounts
將掛載進來的 hostPath 下的全部目錄掛載到 Pod 內的 /data 目錄下spec.serviceAccountName
替換成剛剛創建的 K8s SA 具有 GCS 物件擁有者權限使用指令kubectl apply -f mnist-inference-job.yaml
部署 mnist-inference-job
# mnist-inference-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: mnist-inference-job
namespace: ai
spec:
ttlSecondsAfterFinished: 3600 # Job 將在 3600 秒後刪除
parallelism: 60 # 同時執行 60 個副本
template:
metadata:
name: mnist
spec:
# 替換成剛剛創建的 K8s SA 具有 GCS 物件擁有者權限
serviceAccountName: $Workload_Identity_ServiceAccount
nodeSelector: # 改成所使用的機器標籤,這裡使用 nvidia-h100-80gb
cloud.google.com/gke-accelerator: nvidia-h100-80gb
tolerations:
- key: "nvidia.com/gpu"
operator: "Exists"
effect: "NoSchedule"
containers:
- name: tensorflow
image: tensorflow/tensorflow:latest-gpu
command: ["/bin/bash", "-c", "--"]
args: ["cd /data/tensorflow-mnist-example; pip install -r requirements.txt; python tensorflow_mnist_batch_predict.py"]
resources:
limits:
cpu: "1"
memory: 3Gi
nvidia.com/gpu: "1"
volumeMounts:
# 將掛載進來的 hostPath 下的全部目錄掛載到 Pod 內的 /data 目錄下
- name: hostpath
mountPath: /data
readOnly: true
volumes:
# 將剛剛 Model Sync 的 hostPath 掛載進來
- name: hostpath
hostPath:
path: /mnt/disks/raid/0
type: DirectoryOrCreate
restartPolicy: "Never"
因為我們將每張 H100 都切成 7 份,一台 a3-highgpu-8g
總共有 8 張 H100,所以可以同時部署 7*8=56 個 Pods。
至於此種方法比起 Day23 將 GCS 直接掛到 Pod中又有哪些優點呢?
筆者之所以會寫出這篇文章,也是因為在工作中部署大量的推論用 Ai Model 時遇到的痛點,剛開始服務的 Pod 還不多時可以使用 GCS Sidecar 掛載到每個 Pod 中部署,但是當 GKE 的規模越來越大時,同樣 Model 的 Pod 上百上千時,開始遇到 GCS 網路流量限制的問題,Pod 啟動非常緩慢,甚至有些 Pod 無法成功掛載 GCS,導致服務中斷。
因此,才開始研究如何將 Model 同步到本地硬碟中,並使用 HostPath 掛載到 Pod 中,解決了 GCS 網路流量限制的問題,也大幅提升了 Pod 啟動速度和推理效率。
總而言之,使用本地 NVMe SSD RAID 和 HostPath 掛載模型數據是一種有效的優化方案,可以顯著提升推理性能。 但是,也需要仔細考慮其引入的挑戰,並根據實際情況選擇合適的解決方案。 隨著技術的發展,未來會有更多更完善的方案出現,進一步簡化模型部署和管理。